<?php

namespace Yeelight\Generators;

use Illuminate\Console\DetectsApplicationNamespace;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;

/**
 * Class Generator
 *
 * @category Yeelight
 *
 * @package Yeelight\Generators
 *
 * @author Sheldon Lee <xdlee110@gmail.com>
 *
 * @license https://opensource.org/licenses/MIT MIT
 *
 * @link https://www.yeelight.com
 */
abstract class Generator
{
    use DetectsApplicationNamespace;

    /**
     * The filesystem instance.
     *
     * @var \Illuminate\Filesystem\Filesystem
     */
    protected $filesystem;

    /**
     * The array of options.
     *
     * @var array
     */
    protected $options;

    /**
     * The shortname of stub.
     *
     * @var string
     */
    protected $stub;

    /**
     * Create new instance of this class.
     *
     * @param array $options $options
     */
    public function __construct(array $options = [])
    {
        $this->filesystem = new Filesystem();
        $this->options = $options;
    }

    /**
     * Get the filesystem instance.
     *
     * @return \Illuminate\Filesystem\Filesystem
     */
    public function getFilesystem()
    {
        return $this->filesystem;
    }

    /**
     * Set the filesystem instance.
     *
     * @param \Illuminate\Filesystem\Filesystem $filesystem Filesystem
     *
     * @return $this
     */
    public function setFilesystem(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;

        return $this;
    }

    /**
     * Get stub template for generated file.
     *
     * @return string
     */
    public function getStub()
    {
        $path = config('repository.generator.stubsOverridePath', __DIR__);

        if (!file_exists($path.'/Stubs/'.$this->stub.'.stub')) {
            $path = __DIR__;
        }

        return (new Stub(
            $path . '/Stubs/' . $this->stub . '.stub',
            $this->getReplacements()
        ))->render();
    }

    /**
     * Get template replacements.
     *
     * @return array
     */
    public function getReplacements()
    {
        return [
            'class'          => $this->getClass(),
            'namespace'      => $this->getNamespace(),
            'root_namespace' => $this->getRootNamespace(),
        ];
    }

    /**
     * Get base path of destination file.
     *
     * @return string
     */
    public function getBasePath()
    {
        return base_path();
    }

    /**
     * Get destination path for generated file.
     *
     * @return string
     */
    public function getPath()
    {
        return $this->getBasePath().'/'.$this->getName().'.php';
    }

    /**
     * Get name input.
     *
     * @return string
     */
    public function getName()
    {
        $name = $this->name;
        if (str_contains($this->name, '\\')) {
            $name = str_replace('\\', '/', $this->name);
        }
        if (str_contains($this->name, '/')) {
            $name = str_replace('/', '/', $this->name);
        }

        return Str::studly(
            str_replace(' ', '/', ucwords(str_replace('/', ' ', $name)))
        );
    }

    /**
     * Get name input.
     *
     * @return string
     */
    public function getSnakeName()
    {
        $name = $this->name;
        if (str_contains($this->name, '\\')) {
            $name = str_replace('\\', '/', $this->name);
        }
        if (str_contains($this->name, '/')) {
            $name = str_replace('/', '/', $this->name);
        }

        return Str::plural(
            Str::snake(
                str_replace(' ', '/', ucwords(str_replace('/', ' ', $name)))
            )
        );
    }

    /**
     * Gets id name based on model.
     *
     * @return string
     */
    public function getIdName()
    {
        return str_singular($this->getSnakeName()).'_id';
    }

    /**
     * Get class name.
     *
     * @return string
     */
    public function getClass()
    {
        return Str::studly(class_basename($this->getName()));
    }

    /**
     * Get paths of namespace.
     *
     * @return array
     */
    public function getSegments()
    {
        return explode('/', $this->getName());
    }

    /**
     * Get root namespace.
     *
     * @return string
     */
    public function getRootNamespace()
    {
        return config(
            'repository.generator.rootNamespace',
            $this->getAppNamespace()
        );
    }

    /**
     * Get class-specific output paths.
     *
     * @param string $class className
     * @param bool $directoryPath directoryPath
     *
     * @return \Illuminate\Config\Repository|mixed
     */
    public function getConfigGeneratorClassPath($class, $directoryPath = false)
    {
        switch ($class) {
            case 'models' === $class:
                $path = config('repository.generator.paths.models', 'Entities');
                break;
            case 'repositories' === $class:
                $path = config(
                    'repository.generator.paths.repositories',
                    'Repositories'
                );
                break;
            case 'interfaces' === $class:
                $path = config('repository.generator.paths.interfaces', 'Repositories');
                break;
            case 'presenters' === $class:
                $path = config('repository.generator.paths.presenters', 'Presenters');
                break;
            case 'transformers' === $class:
                $path = config('repository.generator.paths.transformers', 'Transformers');
                break;
            case 'validators' === $class:
                $path = config('repository.generator.paths.validators', 'Validators');
                break;
            case 'controllers' === $class:
                $path = config('repository.generator.paths.controllers', 'Http\Controllers');
                break;
            case 'api_controllers' === $class:
                $path = config('repository.generator.paths.api_controllers', 'Http/Controllers/Api/Controllers');
                break;
            case 'requests' === $class:
                $path = config('repository.generator.paths.requests', 'Http/Requests');
                break;
            case 'provider' === $class:
                $path = config('repository.generator.paths.provider', 'RepositoryServiceProvider');
                break;
            case 'criteria' === $class:
                $path = config('repository.generator.paths.criteria', 'Criteria');
                break;
            case 'views' === $class:
                $path = config('repository.generator.paths.views', 'views');
                break;
            case 'lang' === $class:
                $path = config('repository.generator.paths.criteria', 'lang');
                break;
            default:
                $path = '';
        }

        if ($directoryPath) {
            $path = str_replace('\\', '/', $path);
        } else {
            $path = str_replace('/', '\\', $path);
        }

        return $path;
    }

    /**
     * GetPathConfigNode
     *
     * @return mixed
     */
    abstract public function getPathConfigNode();

    /**
     * Get class namespace.
     *
     * @return string
     */
    public function getNamespace()
    {
        $segments = $this->getSegments();
        array_pop($segments);
        $rootNamespace = $this->getRootNamespace();
        if ($rootNamespace == false) {
            return;
        }

        return 'namespace ' .
            rtrim($rootNamespace . '\\' . implode($segments, '\\'), '\\') .
            ';';
    }

    /**
     * Setup some hook.
     *
     * @return void
     */
    public function setUp()
    {
        //
    }

    /**
     * Run the generator.
     *
     * @throws FileAlreadyExistsException
     *
     * @return int
     */
    public function run()
    {
        $this->setUp();
        if ($this->filesystem->exists($path = $this->getPath()) && !$this->force) {
            throw new FileAlreadyExistsException($path);
        }
        if (!$this->filesystem->isDirectory($dir = dirname($path))) {
            $this->filesystem->makeDirectory($dir, 0777, true, true);
        }

        return $this->filesystem->put($path, $this->getStub());
    }

    /**
     * Get options.
     *
     * @return array | string
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Determinte whether the given key exist in options array.
     *
     * @param string $key Key
     *
     * @return bool
     */
    public function hasOption($key)
    {
        return array_key_exists($key, $this->options);
    }

    /**
     * Get value from options by given key.
     *
     * @param string $key key
     * @param string|null $default default
     *
     * @return string
     */
    public function getOption($key, $default = null)
    {
        if (!$this->hasOption($key)) {
            return $default;
        }

        return $this->options[$key] ?: $default;
    }

    /**
     * Helper method for "getOption".
     *
     * @param string $key key
     * @param string|null $default default
     *
     * @return string
     */
    public function option($key, $default = null)
    {
        return $this->getOption($key, $default);
    }

    /**
     * Handle call to __get method.
     *
     * @param string $key key
     *
     * @return string|mixed
     */
    public function __get($key)
    {
        if (property_exists($this, $key)) {
            return $this->{$key};
        }

        return $this->option($key);
    }
}
